import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class GameClient implements GamePlayerInterface {
	enum GameClientStatus {
		ON_GOING, PRIMARY_CRASH, BACKUP_CRASH
	};

	static String refGameBoard = "GameBoard";
	static String PrimaryServerDown = "Primary Game server down";

	static final int CONNECT_PRIMARY = 1;
	static final int CONNECT_SECONDARY_FIRST_CLIENT = 2; // when primary is
															// down, the first
															// client to detect
															// it
	static final int CONNECT_PRIMARY_AS_PLAYER_1 = 3;
	static final int CONNECT_SECONDARY_OTHER_CLIENT = 4; // after detection of
															// primary down,
															// other clients
															// connect

	static final int HEART_BEAT_INTERVAL = 5000;
	static final int HEART_BEAT_DELAY = 0;
	static final int WAIT_TIME_FOR_PRIMARY_SERVER_ASSIGN = 2000;

	private PlayerMovementInfo myMovementInfo;
	private int myID;
	private MazeGameBoard mazeGameBoard;
	protected Coordinates initPosition;
	private GameboardInterface stubServer;
	private Maze maze;

	private GameClientStatus gcStatus;

	private Timer heartbeatTimer;
	private TimerTask heartbeatTimerTask;
	public boolean heartbeatChecker; //

	// TODO DEBUG_ONLY remove in final version
	int tries = 0;

	// support for RMI timeout
	private ExecutorService executor = null;

	public GameClient(String host, int port) {
		myMovementInfo = new PlayerMovementInfo();
		myMovementInfo.mySequenceNo=0;
		gcStatus = GameClientStatus.ON_GOING;
	}

	public void setID(int id) {
		myID = id;
	}

	public int getID() {
		return myID;
	}

	public void setGameBoardInterface(GameboardInterface stub) {
		stubServer = stub;
	}

	public void Connect(Maze maze, int whichServer) {

		// save the caller
		if (this.maze == null)
			this.maze = maze;

		GameStatusInfo response;

		if ((whichServer == CONNECT_PRIMARY)
				|| (whichServer == CONNECT_PRIMARY_AS_PLAYER_1)) {
			System.out.println("Game Client "
					+ ": Connecting to primary server");
			try {
				response = stubServer.ConnectPlayer(this, whichServer, 0);

				setID(response.GetLastPlayerID());
				int id = getID();

				switch (response.GetGameStatus()) {
				case GAME_IDLE:
					System.out.println("Game Client " + id
							+ ": gameboard idling, waiting for other players ("
							+ initPosition.x + "," + initPosition.y + ")");
					initPosition = new Coordinates(
							response.GetLastPlayerXCoord(),
							response.GetLastPlayerYCoord());
					break;

				case GAME_STARTING:
					initPosition = new Coordinates(
							response.GetLastPlayerXCoord(),
							response.GetLastPlayerYCoord());
					System.out
							.println("Game Client "
									+ id
									+ ": gameboard starting, waiting for other players at ("
									+ initPosition.x + "," + initPosition.y
									+ ")");
					break;

				case GAME_STARTED:
					System.out
							.println("Game Client "
									+ id
									+ " : sorry cannot join. gameboard already started, please come back later ");
					System.exit(0);
					break;
				}

			} catch (RemoteException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} else if (whichServer == CONNECT_SECONDARY_FIRST_CLIENT) {
//				|| whichServer == CONNECT_SECONDARY_OTHER_CLIENT) {
			// TODO: why connect to secondary
			System.out.println("Game Client " + getID()
					+ ": Connecting to new primary server");

			try {
				response = stubServer.ConnectPlayer(this, whichServer, myID);
				if (response == null) {
					System.out.println("GameClient " + myID + ": sleep to wait for new primary setup");
					Thread.sleep(WAIT_TIME_FOR_PRIMARY_SERVER_ASSIGN);
				}
			} catch (RemoteException e) {
				System.err.println("GameClient " + myID + ": Cannot connect to backup server");
				e.printStackTrace();
			} catch (InterruptedException e) {
//				e.printStackTrace();
			}
		}

	}

	/*
	 * Called for all the clients
	 * 
	 * @see GamePlayerInterface#ServerStatusUpdate(GameStatusInfo)
	 */
	public void ServerStatusUpdate(GameStatusInfo gameStatusInfo)
			throws RemoteException {

		if (gameStatusInfo.GetGameStatus() == GameStatusInfo.GameStatus.GAME_STARTED) {

			if (mazeGameBoard == null) {
				mazeGameBoard = new MazeGameBoard(gameStatusInfo.GetMapSize(),
						this);
				// init for timeout RMI
				executor = Executors.newSingleThreadExecutor();
				System.out.println("Game Client " + getID()
						+ ": received from server: "
						+ gameStatusInfo.GetGameStatus().toString());
			}

		} else if (gameStatusInfo.GetGameStatus() == GameStatusInfo.GameStatus.GAME_IDLE) {
			if (mazeGameBoard != null) {
				mazeGameBoard.setEndGame();
				mazeGameBoard.endGame(gameStatusInfo.playerTreasureCount);
				
			}
		}
//		System.out.println("Receive new game status");
		maze.SetGameStatusInfo(gameStatusInfo);
		gcStatus = GameClientStatus.ON_GOING;
	}

	private void handlePrimaryServerCrash() {
		mazeGameBoard.SetCanMove(false);
		maze.InformPrimaryServerDown(myMovementInfo, myID);
		mazeGameBoard.SetCanMove(true);
	}

	public void move(PlayerMovementInfo.Direction drct) {
		int retryies_to_send_to_primary = 0;

		// increment sequence no
		myMovementInfo.mySequenceNo++;		
		myMovementInfo.direction = drct;

		System.out.println("-------------------------------------------------------------------------------");
		
		System.out.println("Game Client " + getID()
				+ ": Send move request " + drct.toString() + " seq no: " + myMovementInfo.mySequenceNo);

		while (retryies_to_send_to_primary < 2) {
			if (retryies_to_send_to_primary != 0)
				System.out.println("Game Client " + getID()
						+ ": Re Send move request " + drct.toString() + " seq no: " + myMovementInfo.mySequenceNo);			
			
			// @RMI_timeout
			if (executor == null) {
				executor = Executors.newSingleThreadExecutor();
			}
			Future<PlayerMovementInfo> future = executor
					.submit(new Callable<PlayerMovementInfo>() {
						@Override
						public PlayerMovementInfo call() throws Exception {
							PlayerMovementInfo response;
							response = stubServer.RequestMove(myMovementInfo,
									myID);
							return response;
						}
					});

			try {
				PlayerMovementInfo response = future.get(Maze.RMI_WAITING_TIME,
						TimeUnit.SECONDS);
				System.out.println("Game Client " + getID()
						+ ": Response from server: " + response.ServerMsg);
				mazeGameBoard.updateGameBoard(response);
				break;
			} catch (InterruptedException | TimeoutException
					| ExecutionException e) {
				// Primary server down
				System.out
						.println("GameClient timeout exception: " + PrimaryServerDown);
				gcStatus = GameClientStatus.PRIMARY_CRASH;
				retryies_to_send_to_primary++;
				handlePrimaryServerCrash();
			} catch (Exception e) {
				e.printStackTrace();
			}

		}
	}

	/*
	 * Received only by Backup Servers
	 * 
	 * @see GamePlayerInterface#SecondaryServerReceiveUPD(PlayerMovementInfo)
	 */
	@Override
	public void SecondaryServerReceiveUPD(PlayerMovementInfo playerMovementInfo)
			throws RemoteException {
		maze.SetPlayerMovementInfo(playerMovementInfo);

	}

	@Override
	public int signalHeartBeat() throws RemoteException {
		heartbeatChecker = true;
		// System.out.println("Receive heartbeat from server");
		initHeartbeat();
		return 0;
	}

	private void initHeartbeat() {
		if (heartbeatTimer == null) {
			heartbeatTimer = new Timer();
			heartbeatTimerTask = new HeartBeat(this);
			heartbeatTimer.schedule(heartbeatTimerTask, HEART_BEAT_DELAY,
					HEART_BEAT_INTERVAL);
		}
	}

	public class HeartBeat extends TimerTask {
		private GameClient gc;

		public HeartBeat(GameClient gclient) {
			gc = gclient;
		}

		@Override
		public void run() {
			if (heartbeatChecker) {
				heartbeatChecker = false;
				return;
			} else {
				// haven't recieved heartbeat for a time -> check
//				System.out.println("haven't recieved heartbeat for a time");
				if (executor == null) {
					executor = Executors.newSingleThreadExecutor();
				}
				Future<Integer> future = executor
						.submit(new Callable<Integer>() {
							@Override
							public Integer call() throws Exception {
								int response = stubServer.signalHeartBeat();
								return response;
							}
						});

				try {
					int response = future.get(Maze.RMI_WAITING_TIME,
							TimeUnit.SECONDS);
				} catch (InterruptedException | TimeoutException
						| ExecutionException e) {
					//TODO: primary crash
					gcStatus = GameClientStatus.PRIMARY_CRASH;
					System.err
							.println("Primary server down - detect by heartbeat");
					handlePrimaryServerCrash();
					// e.printStackTrace();
					// System.err.println(e.getMessage());
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}

	@Override
	public void cancelHeartBeat() {
		if (heartbeatTimerTask != null) {
			System.err.println("Cancel heart beat");
			heartbeatTimerTask.cancel();
			heartbeatTimer.cancel();
			heartbeatTimer.purge();
			heartbeatTimer = null;
		}
	}

	public GameClientStatus getGameClientStatus() {
		return gcStatus;
	}

	@Override
	public boolean ping() throws RemoteException {
		return true;
	}
}
